﻿// -------------------------------------------------------------------------------------------------------------------------------
// <copyright file="CustomSourceAnalyzer.cs" company="Exul">
//     Copyright (c) Exul. All rights reserved.
// </copyright>
// <author>
//     Exul
// </author>
// -------------------------------------------------------------------------------------------------------------------------------

namespace ExulLibrary.Code.Analysis.StyleCop.Rules
{
    #region Usings

    using System;

    using ExulLibrary.Code.Analysis.StyleCop.Rules.Exceptions;

    using global::StyleCop;
    using global::StyleCop.CSharp;

    #endregion

    /// <summary>
    ///     Analyzer for custom StyleCop rules.
    /// </summary>
    [SourceAnalyzer(typeof(CsParser))]
    public sealed class CustomSourceAnalyzer : SourceAnalyzer
    {
        #region Fields

        #region private

        /// <summary>
        ///     The maximal acceptable line length.
        /// </summary>
        private int maxLineLength;

        /// <summary>
        ///     The tab size to replace.
        /// </summary>
        private int tabSize;

        #endregion

        #endregion

        #region Overridden Methods

        #region public

        /// <summary>
        ///     Analyzes the document.
        /// </summary>
        /// <param name="document">
        ///     The document to analyze.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="document"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="DocumentHasUnexpectedTypeException">
        ///     <paramref name="document"/> is not an instance of <see cref="CsDocument"/>.
        /// </exception>
        public override void AnalyzeDocument(CodeDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }

            base.AnalyzeDocument(document);

            this.InitializeSettings(document.Settings);

            var doc = document as CsDocument;

            if (doc == null)
            {
                throw new DocumentHasUnexpectedTypeException("document", document.GetType(), typeof(CsDocument));
            }

            this.AnalyzeSpacing(doc);
        }

        #endregion

        #endregion

        #region Methods

        #region private

        /// <summary>
        ///     Analyzes spacing in the source code.
        /// </summary>
        /// <param name="document">
        ///     The document to analyze.
        /// </param>
        private void AnalyzeSpacing(CsDocument document)
        {
            string text;
            using (var reader = document.SourceCode.Read())
            {
                text = reader.ReadToEnd();
            }

            var splitted = text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);

            for (var i = 0; i < splitted.Length; i++)
            {
                this.CheckEndings(document, splitted[i], i + 1);
                this.CheckLength(document, splitted[i], i + 1);
            }

            this.CheckDocumentEnding(document, text, splitted.Length - 1);
        }

        /// <summary>
        ///     Checks last code line.
        /// </summary>
        /// <param name="document">
        ///     The document to analyze.
        /// </param>
        /// <param name="text">
        ///     The document text.
        /// </param>
        /// <param name="lineNumber">
        ///     The line number.
        /// </param>
        private void CheckDocumentEnding(CsDocument document, string text, int lineNumber)
        {
            if (text.Length == 0)
            {
                return;
            }

            var lastCharacter = text[text.Length - 1];
            if ((lastCharacter == '\r') || (lastCharacter == '\n'))
            {
                this.AddViolation(document.RootElement, lineNumber, "CodeFileMustNotEndWithWhitespace");
            }
        }

        /// <summary>
        ///     Checks the ending of specified code line.
        /// </summary>
        /// <param name="document">
        ///     The document to analyze.
        /// </param>
        /// <param name="currentLine">
        ///     The current line.
        /// </param>
        /// <param name="currentLineNumber">
        ///     The current line number.
        /// </param>
        private void CheckEndings(CsDocument document, string currentLine, int currentLineNumber)
        {
            if (currentLine.Length == 0)
            {
                return;
            }

            if (!char.IsWhiteSpace(currentLine[currentLine.Length - 1]))
            {
                return;
            }

            this.AddViolation(document.RootElement, currentLineNumber, "CodeLineMustNotEndWithWhitespace");
        }

        /// <summary>
        ///     Checks length of the specified code line.
        /// </summary>
        /// <param name="document">
        ///     The document to analyze.
        /// </param>
        /// <param name="currentLine">
        ///     The current line.
        /// </param>
        /// <param name="currentLineNumber">
        ///     The current line number.
        /// </param>
        private void CheckLength(CsDocument document, string currentLine, int currentLineNumber)
        {
            var replacement = new string(' ', this.tabSize);
            currentLine = currentLine.Replace("\t", replacement);

            if (this.maxLineLength < currentLine.Length)
            {
                this.AddViolation(
                    document.RootElement,
                    currentLineNumber,
                    "LineIsTooLong",
                    this.maxLineLength,
                    currentLine.Length);
            }
        }

        /// <summary>
        ///     Initializes the settings.
        /// </summary>
        /// <param name="settings">
        ///     The settings.
        /// </param>
        private void InitializeSettings(Settings settings)
        {
            var lengthProperty = this.GetSetting(settings, "LineLength");
            if (lengthProperty == null)
            {
                throw new SettingsHasNoRequiredPropertyException("settings", "LineLength");
            }

            var lengthValue = lengthProperty as IntProperty;
            if (lengthValue == null)
            {
                throw new PropertyHasUnexpectedTypeException(
                    "settings", "LineLength", lengthProperty.GetType(), typeof(IntProperty));
            }

            this.maxLineLength = lengthValue.Value;

            var tabSizeProperty = this.GetSetting(settings, "TabSize");
            if (tabSizeProperty == null)
            {
                throw new SettingsHasNoRequiredPropertyException("settings", "TabSize");
            }

            var tabSizeValue = tabSizeProperty as IntProperty;
            if (tabSizeValue == null)
            {
                throw new PropertyHasUnexpectedTypeException(
                    "settings", "TabSize", tabSizeProperty.GetType(), typeof(IntProperty));
            }

            this.tabSize = tabSizeValue.Value;
        }

        #endregion

        #endregion
    }
}